-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support Ruby 3.2, 3.3 and Rails 7.1 #3
Conversation
``` 1) ActiveRecord::DebugErrors::DisplayConnectionOwners#execute when ActiveRecord::Deadlocked occurs when the user has the permission to execute 'SHOW ENGINE INNODB STATUS' displays latest detected deadlock Failure/Error: ActiveRecord::Base.legacy_connection_handling = false NoMethodError: undefined method `legacy_connection_handling=' for class ActiveRecord::Base # ./vendor/bundle/ruby/3.3.0/gems/activerecord-7.1.3.3/lib/active_record/dynamic_matchers.rb:22:in `method_missing' # ./spec/spec_helper.rb:29:in `block (2 levels) in <top (required)>' ```
Oops, I found the latest Rails breaks something... Let me check the CI failures. |
…ecute` Starting with Rails 7.1, `raw_execute` may not be called when executing a query. This pull request introduces `with_raw_connection`, and this method is now called when executing a query. https://github.com/rails/rails/pull/44576/files#diff-460f4e7973c5dd945c51d24df5b0173961190d3645f4e2585fd3003fa1fc0ff7R865 Overriding this method and calling super will result in a LocalJumpError, which is not safe. In this commit, I tried to call the log by hooking into `translate_exception_class`. This method has been around since 4.2, and there should be no changes to the interface since 6.0. rails/rails@5e5118a#diff-460f4e7973c5dd945c51d24df5b0173961190d3645f4e2585fd3003fa1fc0ff7R356
`acquire_connection` in connection_pool is now smarter in Rails 7.1.3. Even when running existing tests in Rails7.1.3, `ActiveRecord::ConnectionTimeoutError` no longer occurs because a connection can be obtained by `try_to_checkout_new_connection` after reap. https://github.com/rails/rails/compare/v7.1.2..v7.1.3#diff-642b90553b888bd2c724c093a1a685a5408a7d8293f3751366c25dc548936eb7R660
bf0b8b7
to
6214941
Compare
Although the diff got larger than I expected, I was finally able to pass test for all supported versions: 6.0, 6,1, 7.0, 7.1. See passed tests here: https://github.com/ohbarye/activerecord-debug_errors/actions/runs/9242922830 |
raise | ||
end | ||
private method_name if method_name == :raw_execute | ||
elsif ActiveRecord.version >= Gem::Version.new("7.1.0") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As commented in 5f9a8c9, there is a case when neither raw_execute
nor execute
is called in Rails 7.1.
I'm not sure if this way is the best, I tried to use translate_exception_class
as a hook for Rails 7.1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the following approach is similar to the original one and more readable. What do you think about them?
diff --git a/lib/activerecord/debug_errors/ext/connection_adapters/abstract_mysql_adapter.rb b/lib/activerecord/debug_errors/ext/connection_adapters/abstract_mysql_adapter.rb
index 77f3f25..680e2e6 100644
--- a/lib/activerecord/debug_errors/ext/connection_adapters/abstract_mysql_adapter.rb
+++ b/lib/activerecord/debug_errors/ext/connection_adapters/abstract_mysql_adapter.rb
@@ -80,6 +80,12 @@ module ActiveRecord
end
end
+ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.descendants.each do |adapter|
+ adapter.prepend(ActiveRecord::DebugErrors::DisplayMySQLInformation)
+end
class ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
- prepend ActiveRecord::DebugErrors::DisplayMySQLInformation
+ def self.inherited(base)
+ super
+ base.prepend(ActiveRecord::DebugErrors::DisplayMySQLInformation)
+ end
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the code you presented is quite neat!
I think, I'm probably misunderstanding something big (and my implementation has based on the misunderstanding), but could you please explain to me why your code still works in Rails 7.1 for further studies? (and why the original code doesn't work.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As you can see below, ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#raw_execute
is not implemented, whereas the subclasses have the method raw_execute
, so appending the module to the subclasses works expectedly:
% bundle exec irb
irb(main):001> require "active_record"
=> true
irb(main):002> require "active_record/connection_adapters/abstract_mysql_adapter"
=> true
irb(main):003> require "active_record/connection_adapters/mysql2_adapter"
=> true
irb(main):004> require "active_record/connection_adapters/trilogy_adapter"
=> true
irb(main):005> show_source ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#raw_execute
From: /Users/arabiki/ghq/src/github.com/abicky/activerecord-debug_errors/gemfiles/vendor/bundle/ruby/3.3.0/gems/activerecord-7.1.3.3/lib/active_record/connection_adapters/abstract/database_statements.rb:530
def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
raise NotImplementedError
end
=> nil
irb(main):006> show_source ActiveRecord::ConnectionAdapters::Mysql2Adapter#raw_execute
From: /Users/arabiki/ghq/src/github.com/abicky/activerecord-debug_errors/gemfiles/vendor/bundle/ruby/3.3.0/gems/activerecord-7.1.3.3/lib/active_record/connection_adapters/mysql2/database_statements.rb:96
def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
log(sql, name, async: async) do
with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
sync_timezone_changes(conn)
result = conn.query(sql)
verified!
handle_warnings(sql)
result
end
end
end
=> nil
irb(main):007> show_source ActiveRecord::ConnectionAdapters::TrilogyAdapter#raw_execute
From: /Users/arabiki/ghq/src/github.com/abicky/activerecord-debug_errors/gemfiles/vendor/bundle/ruby/3.3.0/gems/activerecord-7.1.3.3/lib/active_record/connection_adapters/trilogy/database_statements.rb:44
def raw_execute(sql, name, async: false, allow_retry: false, materialize_transactions: true)
log(sql, name, async: async) do
with_raw_connection(allow_retry: allow_retry, materialize_transactions: materialize_transactions) do |conn|
sync_timezone_changes(conn)
result = conn.query(sql)
verified!
handle_warnings(sql)
result
end
end
end
=> nil
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I got it now. Thanks for your explanation!
Thread.new { sleep 10 } # another thread | ||
|
||
expect { | ||
ActiveRecord::Base.connection # Ensure to acquire a connection | ||
Array.new(ActiveRecord::Base.connection_pool.size) do | ||
Thread.new do | ||
ActiveRecord::Base.connection_pool.checkout(0.1) | ||
ActiveRecord::Base.connection_pool.checkout | ||
sleep 0.001 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Connection pool handling in Rails 7.1 is so smarter that the existing test cannot cause ConnectionTimeoutError
. To cause it, I tweaked the code.
ref: 1688bd0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm afraid that this test becomes flaky because some threads might end before the last thread calls checkout
. How about the following changes?
diff --git a/spec/activerecord/debug_errors/ext/connection_adapters/connection_pool_spec.rb b/spec/activerecord/debug_errors/ext/connection_adapters/connection_pool_spec.rb
index 7a86bc7..6f2b1fe 100644
--- a/spec/activerecord/debug_errors/ext/connection_adapters/connection_pool_spec.rb
+++ b/spec/activerecord/debug_errors/ext/connection_adapters/connection_pool_spec.rb
@@ -20,11 +20,20 @@ RSpec.describe ActiveRecord::DebugErrors::DisplayConnectionOwners do
it "displays connection owners and other threads" do
Thread.new { sleep 10 } # another thread
+ mutex = Mutex.new
+ cv = ConditionVariable.new
+
expect {
ActiveRecord::Base.connection # Ensure to acquire a connection
Array.new(ActiveRecord::Base.connection_pool.size) do
Thread.new do
- ActiveRecord::Base.connection_pool.checkout(0.1)
+ mutex.synchronize do
+ ActiveRecord::Base.connection_pool.checkout(0.1)
+ cv.wait(mutex, 1)
+ rescue
+ cv.broadcast
+ raise
+ end
end
end.each(&:join)
}.to raise_error(ActiveRecord::ConnectionTimeoutError)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly. And to be honest, I couldn't think of any other way to do it, so I'm glad to know the cool way you suggested it. Updated d2a4654
# For compatibility. Rails deprecated since 6.1 and removed this option since 7.1. | ||
# https://github.com/rails/rails/pull/44827/commits/ad52c0a19714a1b87a7d0c626a8b364cf95414cf | ||
if ActiveRecord::Base.respond_to?(:legacy_connection_handling) | ||
ActiveRecord::Base.legacy_connection_handling = false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is simply broken in Rails 7.1.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for your pull request! I appreciate your contribution.
I've checked the changes and made some comments.
raise | ||
end | ||
private method_name if method_name == :raw_execute | ||
elsif ActiveRecord.version >= Gem::Version.new("7.1.0") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the following approach is similar to the original one and more readable. What do you think about them?
diff --git a/lib/activerecord/debug_errors/ext/connection_adapters/abstract_mysql_adapter.rb b/lib/activerecord/debug_errors/ext/connection_adapters/abstract_mysql_adapter.rb
index 77f3f25..680e2e6 100644
--- a/lib/activerecord/debug_errors/ext/connection_adapters/abstract_mysql_adapter.rb
+++ b/lib/activerecord/debug_errors/ext/connection_adapters/abstract_mysql_adapter.rb
@@ -80,6 +80,12 @@ module ActiveRecord
end
end
+ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.descendants.each do |adapter|
+ adapter.prepend(ActiveRecord::DebugErrors::DisplayMySQLInformation)
+end
class ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
- prepend ActiveRecord::DebugErrors::DisplayMySQLInformation
+ def self.inherited(base)
+ super
+ base.prepend(ActiveRecord::DebugErrors::DisplayMySQLInformation)
+ end
end
Thread.new { sleep 10 } # another thread | ||
|
||
expect { | ||
ActiveRecord::Base.connection # Ensure to acquire a connection | ||
Array.new(ActiveRecord::Base.connection_pool.size) do | ||
Thread.new do | ||
ActiveRecord::Base.connection_pool.checkout(0.1) | ||
ActiveRecord::Base.connection_pool.checkout | ||
sleep 0.001 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm afraid that this test becomes flaky because some threads might end before the last thread calls checkout
. How about the following changes?
diff --git a/spec/activerecord/debug_errors/ext/connection_adapters/connection_pool_spec.rb b/spec/activerecord/debug_errors/ext/connection_adapters/connection_pool_spec.rb
index 7a86bc7..6f2b1fe 100644
--- a/spec/activerecord/debug_errors/ext/connection_adapters/connection_pool_spec.rb
+++ b/spec/activerecord/debug_errors/ext/connection_adapters/connection_pool_spec.rb
@@ -20,11 +20,20 @@ RSpec.describe ActiveRecord::DebugErrors::DisplayConnectionOwners do
it "displays connection owners and other threads" do
Thread.new { sleep 10 } # another thread
+ mutex = Mutex.new
+ cv = ConditionVariable.new
+
expect {
ActiveRecord::Base.connection # Ensure to acquire a connection
Array.new(ActiveRecord::Base.connection_pool.size) do
Thread.new do
- ActiveRecord::Base.connection_pool.checkout(0.1)
+ mutex.synchronize do
+ ActiveRecord::Base.connection_pool.checkout(0.1)
+ cv.wait(mutex, 1)
+ rescue
+ cv.broadcast
+ raise
+ end
end
end.each(&:join)
}.to raise_error(ActiveRecord::ConnectionTimeoutError)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, thank you for your contribution! Your investigation helped a lot.
I wonder if it's okay to remove 2.6 to 3.0 support since they are already EOL.
I think we can stop supporting the versions but it might be better not to deal with the issue on this PR.
I've released v0.1.3. |
Agree. Thanks for your quick review and release too! |
Hi @abicky ,
Changes
My first motivation is to run tests against Ruby 3.2 & 3.3. But I found that this gem doesn't work with Rails 7.1, so I created some patches for that and added version combinations to the test matrix.
Thoughts
I wonder if it's okay to remove 2.6 to 3.0 support since they are already EOL.
https://endoflife.date/ruby